/* * ***** BEGIN LICENSE BLOCK *****
 *
 * The contents of this file are subject to the BitTorrent Open Source License
 * Version 1.0 (the License).  You may not copy or use this file, in either
 * source code or executable form, except in compliance with the License.  You
 * may obtain a copy of the License at http://www.bittorrent.com/license/.
 *
 * Software distributed under the License is distributed on an AS IS basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Original Written by Petru Paler
 * Translated to Javascript by Matthew Mead Armsby
 * Version: 2009-02-27
 *
 * **** END LICENSE BLOCK ***** */

// Grab XPCOMUtils. This limits this component to Firefox 3.
// See https://developer.mozilla.org/en/How_to_Build_an_XPCOM_Component_in_Javascript
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

/**
 * Decodes a bencoded int to an actual int
 * @param x a bencoded integer
 * @param f the current position in x
 * @throws ValueError: decoding int: found negative zero
 * @throws ValueError: decoding int: found leading zero
 * @return Array[int, size]: an array containing the integer and its size
 */  
function decode_int(x, f) {
    // f is current index in string x.
    f += 1;
    // newf is the index of the end of the current integer in x
    var newf = x.indexOf('e', f);
    // We're grabbing the stuff before e, and converting it to an integer
    var n = parseInt(x.substring(f,newf+1));
    // Handle negative
    if (x.charAt(f) == '-') {
        if (x.charAt(f + 1) == '0') {
               throw Components.Exception("ValueError: decoding int: found negative zero");
        }
    // Handle leading 0s
    } else if (x.charAt(f) == '0' && newf != (f+1)){
           throw Components.Exception("ValueError: decoding int: found leading zero");
    }
    return [n, newf+1];
}

/**
 * Decodes a bencoded string to an actual string
 * @param x a bencoded string
 * @param f the current position in x
 * @throws ValueError: decoding string: string length has leading 0 (or gap between 0 and colon)
 * @return Array[string, size]: an array containing the string and its size
 */
function decode_string(x, f){
    var colon = x.indexOf(':', f);
    var n = parseInt(x.substring(f,colon+1));
    if (x.charAt(f) == '0' && colon != (f+1)) {
        throw Components.Exception("ValueError: decoding string: string length has leading 0 (or gap between 0 and colon)");
    }
    colon += 1;
    return [x.substring(colon,colon+n), colon+n];
}

/**
 * Decodes a bencoded list (i.e. an Array) into an actual list
 * @param x a bencoded list
 * @param f the current position in x
 * @return Array[list, size]: an array containing the list and its size
 */
function decode_list(x, f){
    var r = [];
    f = f+1;
    var tmp;
    while (x.charAt(f) != 'e') {
        tmp = decode_func[x[f]](x, f);
        v = tmp[0]; f = tmp[1]; 
        // NOTE: Push on Javascript array, append on Python array
        r.push(v);
    }
    return [r, f + 1];
}

/**
 * Decodes a bencoded dictionary (i.e. a hash table) into an actual dictionary
 * @param x a bencoded dictionary
 * @param f the current position in x
 * @throws ValueError: decoding dict: dictionary out of order (index N, Found KEY after LASTKEY)
 * @return Array[dict, size]: an array containing the dictionary and its size
 */
function decode_dict(x, f){
    var r = {};
    var k;
    f = f+1;
    var lastkey = null;
    var tmp;
    while (x.charAt(f) != 'e'){
        tmp = decode_string(x, f);
        k = tmp[0]; f = tmp[1];
        if (lastkey >= k) {
            throw Components.Exception("ValueError: decoding dict: "
                + "dictionary out of order (index " + f + ", "
                + "Found " + k + " after " + lastkey + ")"
            );
        }
        lastkey = k;
        tmp = decode_func[x[f]](x, f);
        r[k] = tmp[0];
        f = tmp[1];
    }
    return [r, f + 1];
}

/**
 * A hash table for determining which decode function to call
 * <br><p style="margin-left:1em">
 * <b> Keymap: </b><blockquote>
 *
 * 'l' => decode_list() <br>
 * 'd' => decode_dict() <br>
 * 'i' => decode_int() <br>
 * '0'-'9' => decode_string()
 *
 * </blockquote></p>
 */
decode_func = {};
decode_func['l'] = decode_list;
decode_func['d'] = decode_dict;
decode_func['i'] = decode_int;
decode_func['0'] = decode_string;
decode_func['1'] = decode_string;
decode_func['2'] = decode_string;
decode_func['3'] = decode_string;
decode_func['4'] = decode_string;
decode_func['5'] = decode_string;
decode_func['6'] = decode_string;
decode_func['7'] = decode_string;
decode_func['8'] = decode_string;
decode_func['9'] = decode_string;

/**
 * Decodes a bencoded variable
 * @param x a bencoded value to decode
 * @throws ValueError: not a string
 * @throws ValueError: can't decode empty string
 * @throws ValueError: unknown type, couldn't find decode function
 * @throws ValueError: not a valid bencoded string
 * @throws ValueError: invalid bencoded value (data after valid end (whitespace?))
 * @return return value of the selected decode function
 */
function bdecode(x) {
    var l;
    var r;
    var e;
    var tmp;
    if (bencode_typeof(x) != "string") {
        throw Components.Exception("ValueError: not a string");
    }
    if (x.length == 0) {
        throw Components.Exception("ValueError: can't decode empty string");
    }
    if (!(x[0] in decode_func)) {
        // couldn't find decode func
        throw Components.Exception("ValueError: unknown type, couldn't find decode function");
    }
    tmp = decode_func[x[0]](x, 0);
    r = tmp[0]; 
    l = tmp[1];
    if (l != x.length) {
        throw Components.Exception("ValueError: invalid bencoded value (data after valid end (whitespace?))");
    }
    return r;
}

//from types import StringType, IntType, LongType, DictType, ListType, TupleType

// We don't have Bencached working.
// The idea is (woah!) that you can cache the encoded value.
// Not actually required for bencoding, it's just a time saver.

/**
 * Caches a bencoded value
 * @param s the bencoded value to cache
 */
Bencached = function(s) {
    this.bencoded = s;
    // TODO: can we enforece equivalent to __slots__ ?
    // http://www.network-theory.co.uk/docs/pylang/__slots__.html
    // __slots__ = ["bencoded"]
    
}

/**
 * Pushes a bencoded bencached value onto an array
 * @param x an instance of Bencode
 * @param r a reference to an Array
 */
function encode_bencached(x,r){
    // r is an array
    r.push(x.bencoded);
}

/**
 * Pushes a bencoded integer onto an array
 * @param x an instance of Bencode
 * @param r a reference to an Array
 */
function encode_int(x, r){
    // r is an array
    // here we add to the r array using push. Push takes n arguments, adds in order.
    r.push("i", x, "e");
}

/**
 * Pushes a bencoded string onto an array
 * @param x an instance of Bencode
 * @param r a reference to an Array
 */
function encode_string(x, r){
    // r is an array
    r.push(x.length, ':', x)
}

/**
 * Pushes a bencoded list (i.e. an Array) onto an Array
 * @param x an instance of Bencode
 * @param r a reference to an Array
 */
function encode_list(x, r){
    // r is an array.
    r.push('l'); 
    for (var i = 0; i < x.length; i++) {
        // TODO: ensure typeof works
        encode_func[bencode_typeof(x[i])](x[i], r);
    }
    r.push("e");
}

/**
 * Pushes a bencoded dictionary (i.e. a Hash Table) onto an Array
 * @param x an instance of Bencode
 * @param r a reference to an Array
 */
function encode_dict(x,r){
    r.push("d");
    /*var ilist = x.items();
    ilist.sort()
    
    for k, v in ilist:
        r.extend((str(len(k)), ':', k))
        encode_func[type(v)](v, r)
    */
    var k;
    var v;
    // This NEEDS to be in order.
    var l = [];
    for (k in x) {
        l.push(k);
    }
    l.sort();
    // Now in order, we can create encoded form
    while (l.length > 0) {
        k = l.shift();
        v = x[k];
        r.push(k.length + "", ":", k);
        // TODO: ensure typeof works
        encode_func[bencode_typeof(v)](v, r);        
    }
    r.push("e");
}

// TODO: Note this is a new function to use instead of typeof(), as 
//  typeof is causing probs
// Also note there is a duplicate of this function (excluding 
//  Bencached) in generalmain of the bittorrent handler.
// This is because we need a handy function to get types, that draws
// a distinction between arrays and other objects.

/**
 * Returns the type of a bencoded object
 * @param o a bencoded object
 * @return The type of the object, can either be "array" or "bencached"
 */
bencode_typeof = function(o) {
    var t = typeof(o);
    if (t=="object"){
        // We care about particular types
        if (o instanceof Array) {
            t = "array";
        } else if (o instanceof Bencached) {
            t = "bencached";
        }
    }
    return t;
}

/**
 * A hash table for determining which encode function to call
 * <br><p style="margin-left:1em">
 * <b> Keymap: </b><blockquote>
 *
 * 'bencached' => encode_bencached() <br>
 * 'number' => encode_int() <br>
 * 'string' => encode_string() <br>
 * 'array' => encode_list() <br>
 * 'object' => encode_dict() <br>
 * 'boolean' => encode_int()
 *
 * </blockquote></p>
 */
encode_func = {};
// TODO: handle Bencached somehow
encode_func["bencached"] = encode_bencached;
encode_func["number"] = encode_int;
// JS only has number type, don't draw a long/int distinction: encode_func[LongType] = encode_int
encode_func["string"] = encode_string;
encode_func["array"] = encode_list;
// JS has no tuples: encode_func[TupleType] = encode_list
encode_func["object"] = encode_dict;
// TODO: Check encoding booleans as ints works. This is rare.
encode_func["boolean"] = encode_int;

/**
 * Bencodes a value
 * @param x the value to bencode
 * @throws ValueError: bencode: no encoding function for type: type was <bencode_typeof(x)>
 * @return return value of the selected function
 */
function bencode(x){
    var r = [];
    if (bencode_typeof(x) in encode_func) {
        encode_func[bencode_typeof(x)](x, r);
    } else {
        throw Components.Exception("ValueError: bencode: "
            + "no encoding function for type: "
            + "type was " + bencode_typeof(x));
    }
    return r.join("");
}

/**
 * A class for bencoding and bdecoding data.
 * A conversion of MainLine's Bencoding functions originally authored by Petru Paler.
 * @see <a href="http://wiki.theory.org/BitTorrentSpecification#bencoding">Theory.org's wiki article</a>
 * @see <a href="http://pypi.python.org/pypi/BitTorrent-bencode/5.0.8">MainLine's Bencoding</a>
 * @constructor
 */
Bencoding = function() {};

Bencoding.prototype = {
    classDescription : "Bencoding",
    contractID : "@wikiscraps.com/encoding/bencoding;1",
    classID : Components.ID("d430638d-80b3-47ad-b9d9-5d94be758491"),
    QueryInterface : XPCOMUtils.generateQI(
        [Components.interfaces.Bencoding
        ,Components.interfaces.nsISupports]),
/**
 * Bencodes some data
 * @param data Some data to bencode
 * @return the bencoded data
 */   
    bencode : function(data) { return bencode(data); },

/**
 * Bdecodes an object
 * @param obj An object to bdecode
 * @returns The bdecoded object
 */
    bdecode : function(obj) { return bdecode(obj); }
}

var NSGetModule = XPCOMUtils.generateNSGetModule([Bencoding]);

